<?php

namespace DeCMF\Core;

use DeCMF\Core\Validator;

/**
 * 通用Model模型类,用于纯数据模型场景
 * 主要特性:
 * 1. 属性定义与访问
 * 2. 数据校验\自动填充\字段自动映射
 * 
 * 常见用法:
 * <pre>
 * class Student extends Model
 * {
 *      protected $name = 'student';
 *      protected $fields = [
 *          'id', 'truename', 'gender', 'age', 'address', 'added_time'
 *      ];
 * 
 *      
 *      protected $_validate = [
 *          ['truename', '/^[a-zA-Z]\w{0,30}$/', '名称不合法', self::VALUE_VALIDATE, 'regex', self::MODEL_BOTH],
 *          ......
 *      ],
 *      protected $_auto = [
 *          ['address', 'getAddress', self::MODEL_BOTH, 'callback'],
 *          ......
 *      ];
 * }
 * 
 * $model = new Student();
 * if (!$model->create(I('post.')) && !$model->add()) {
 *      $this->error($model->getError());
 * }
 * </pre>
 */
class Model
{
    // 模型内置场景类型

    /**
     * 插入模型数据
     */
    const MODEL_INSERT = 1;
    
    /**
     * MODEL_INSERT的别名
     */
    const Insert = self::MODEL_INSERT;

    /**
     * 更新模型数据
     */
    const MODEL_UPDATE = 2;
    
    /**
     * MODEL_UPDATE的别名
     */
    const Update = self::MODEL_UPDATE;

    /**
     * 包含插入和更新两种方式
     */
    const MODEL_BOTH = 3;
    
    /**
     * MODEL_BOTH的别名
     */
    const Both = self::MODEL_BOTH;

    /**
     * 表单数据存在该字段则验证
     */
    const EXISTS_VALIDATE = 0;
    
    /**
     * EXISTS_VALIDATE别名 表单数据存在该字段则验证 isset
     */
    const Existed = self::EXISTS_VALIDATE;

    /**
     * 该字段必须验证
     */
    const MUST_VALIDATE = 1;
    
    /**
     * MUST_VALIDATE别名,该字段必须验证
     */
    const Ever = self::MUST_VALIDATE;

    /**
     * 表单值不为空则验证
     */
    const VALUE_VALIDATE = 2;
    
    /**
     * VALUE_VALIDATE别名 表单值不为空则验证 不是空字符串或全由空格组成
     */
    const NotBlank = self::VALUE_VALIDATE;

    /**
     *
     * @var string 模型名称
     */
    protected $name = '';
    
    /**
     *
     * @var string|array 最近错误信息
     */
    protected $error = '';
    
    /**
     *
     * @var array 字段信息
     */
    protected $fields = array();
    
    /**
     *
     * @var array 数据信息
     */
    protected $data = array();
    
    /**
     *
     * @var array 自动验证规则定义
     */
    protected $_validate = array();
    
    /**
     *
     * @var array 自动完成定义
     */
    protected $_auto = array();
    
    /**
     *
     * @var array 字段映射定义
     */
    protected $_map = array();
    
    /**
     *
     * @var boolean 是否自动检测数据表字段信息
     */
    protected $autoCheckFields = true;
    
    /**
     *
     * @var boolean 是否批处理验证
     */
    protected $patchValidate = false;
    
    /**
     * 原Think\Model中的options属性的含义已被拆分
     * 在Model基类中,这个属性主要存储一些临时性数据,如auto开关 token校验开关 validate临时校验规则
     * 而构造db sql查询的相关参数被放到Query类中
     * @var array 
     */
    protected $options = [];

    /**
     * 构造函数
     * @param string $name 模型名称
     */
    public function __construct($name = '')
    {
        // 获取模型名称
        if (!empty($name)) {
            $this->name = $name;
        } elseif (empty($this->name)) {
            $this->name = $this->getModelName();
        }

        // 初始化
        $this->_initialize();
    }

    /**
     * 设置数据对象的值
     *
     * @param string $name 名称
     * @param mixed $value 值
     */
    public function __set($name, $value)
    {
        // 设置数据对象属性
        $this->data[$name] = $value;
    }

    /**
     * 获取数据对象的值
     *
     * @param string $name 名称
     * @return mixed
     */
    public function __get($name)
    {
        return isset($this->data[$name]) ? $this->data[$name] : null;
    }

    /**
     * 检测数据对象的值
     *
     * @param string $name 名称
     * @return boolean
     */
    public function __isset($name)
    {
        return isset($this->data[$name]);
    }

    /**
     * 销毁数据对象的值
     *
     * @param string $name 名称
     */
    public function __unset($name)
    {
        unset($this->data[$name]);
    }

    /**
     * 利用__call方法实现一些特殊的Model方法
     * @param string $method 方法名称
     * @param array $args 调用参数
     * @return mixed
     */
    public function __call($method, $args)
    {
        E(__CLASS__ . ':' . $method . L('_METHOD_NOT_EXIST_'));
    }

    /**
     * 回调方法 初始化模型
     */
    protected function _initialize()
    {
        
    }
    
    /**
     * 添加一条数据验证规则,可用于_initialize()方法中初始化,或动态添加规则等
     * 该方法兼容TP Model的规则书写顺序
     * 为了避免因为大量的常量写法导致整个规则列表没有美感,
     * 专门封装了addInsertRule addUpdateRule addBothRule这几个方法
     * 这三个方法中,简化了TP原有的$when的概念: require时,$when为Ever;其它情况$when为exists
     * @param string $field 实际的数据表字段而不是表单字段 多个字段以逗号分隔
     * @param string $rule 规则类型下对应的值,如 'require' 正则规则,简称,声明必填
     * @param string $errorTip 验证不通过时的提示语
     * @param int $when 三种 Existed(默认) Ever NotEmpty 参看 *_VALIDATE常量
     * @param string $ruleType 规则类型 见Validator类常量 默认值 REGEX
     * @param int $on 3种场景scenario,Insert Update Both(默认),参看MODEL_*常量,可以自行定义
     * @return $this
     */
    public function addRule($field, $rule, $errorTip, $when = self::Existed, $ruleType = Validator::REGEX, $on = self::MODEL_BOTH)
    {
        $this->_validate[] = [$field, $rule, $errorTip, $when, $ruleType, $on];
        
        return $this;
    }
    
    /**
     * 添加一条数据自动填充规则,可用于_initialize()方法中初始化,或动态添加规则等
     * 该方法兼容TP Model的规则书写顺序
     * 为了避免因为大量的常量写法导致整个规则列表没有美感,
     * 专门封装了addInsertAuto addUpdateAuto addBothAuto这几个方法
     * @param string $field 字段名
     * @param mixed $rule 如 md5
     * @param int $on 3种场景scenario,scenario,Insert(默认) Update Both,参看MODEL_*常量,可以自行定义
     * @param string $ruleType function|callback|field|ignore|string(默认)
     * @param mixed $_ 更多动态参数,供$rule使用
     * @return $this
     */
    public function addAuto($field, $rule, $on = self::Insert, $ruleType = 'string')
    {
        $this->_auto[] = [$field, $rule, $on, $ruleType];
        
        return $this;
    }
    
    /**
     * 添加一条数据验证规则
     * 
     * @param string $field 参照addRule
     * @param string $rule 规则类型下对应的值
     * @param string $errorTip 提示语
     * @param string $ruleType 规则类型 见Validator类常量 默认值 REGEX
     * @param int $when 三种 Existed(默认) Ever NotEmpty 参看 *_VALIDATE常量
     * @return $this
     */
    public function addBothRule($field, $rule, $errorTip, $ruleType = Validator::REGEX, $when = self::Existed)
    {

        return $this->addRule($field, $rule, $errorTip, $when, $ruleType, self::Both);
    }
    
    /**
     * 添加一条数据验证规则
     * 
     * @param string $field 参照addRule
     * @param string $rule 规则类型下对应的值
     * @param string $errorTip 提示语
     * @param string $ruleType 规则类型 见Validator类常量 默认值 REGEX
     * @param int $when 三种 Existed(默认) Ever NotEmpty 参看 *_VALIDATE常量
     * @return $this
     */
    public function addInsertRule($field, $rule, $errorTip, $ruleType = Validator::REGEX, $when = self::Existed)
    {
        return $this->addRule($field, $rule, $errorTip, $when, $ruleType, self::Insert);
    }
    
    /**
     * 添加一条数据验证规则
     * 
     * @param string $field 参照addRule
     * @param string $rule 规则类型下对应的值
     * @param string $errorTip 提示语
     * @param string $ruleType 规则类型 见Validator类常量 默认值 REGEX
     * @param int $when 三种 Existed(默认) Ever NotEmpty 参看 *_VALIDATE常量
     * @return $this
     */
    public function addUpdateRule($field, $rule, $errorTip, $ruleType = Validator::REGEX, $when = self::Existed)
    {
        return $this->addRule($field, $rule, $errorTip, $when, $ruleType, self::Update);
    }
    
    /**
     * 添加一条插入场景的数据自动填充规则
     * 
     * @param string $field 字段名
     * @param mixed $rule 如 md5
     * @param string $ruleType function|callback|field|ignore|string(默认)
     * @param mixed $_ 更多动态参数,供$rule使用
     * @return $this
     */
    public function addInsertAuto($field, $rule, $ruleType = 'string')
    {
        return $this->addAuto($field, $rule, self::Insert, $ruleType);
    }
    
    /**
     * 添加一条更新场景的数据自动填充规则
     * 
     * @param string $field 字段名
     * @param mixed $rule 如 md5
     * @param string $ruleType function|callback|field|ignore|string(默认)
     * @param mixed $_ 更多动态参数,供$rule使用
     * @return $this
     */
    public function addUpdateAuto($field, $rule, $ruleType = 'string')
    {
        return $this->addAuto($field, $rule, self::Update, $ruleType);
    }
    
    /**
     * 添加一条两种场景都适用的数据自动填充规则
     * 
     * @param string $field 字段名
     * @param mixed $rule 如 md5
     * @param string $ruleType function|callback|field|ignore|string(默认)
     * @param mixed $_ 更多动态参数,供$rule使用
     * @return $this
     */
    public function addBothAuto($field, $rule, $ruleType = 'string')
    {
        return $this->addAuto($field, $rule, self::Both, $ruleType);
    }

    /**
     * 返回待输出结果
     * @param array $data
     * @param string $type
     * @return string
     */
    public function returnResult($data, $type = '')
    {
        if ($type) {
            if (is_callable($type)) {
                return call_user_func($type, $data);
            }
            switch (strtolower($type)) {
                case 'json':
                    return json_encode($data);
                case 'xml':
                    return xml_encode($data);
            }
        }
        return $data;
    }

    /**
     * 处理字段映射
     *
     * @param array $data 当前数据
     * @param integer $type 类型 0 写入 1 读取
     * @return array
     */
    public function parseFieldsMap($data, $type = 1)
    {
        // 检查字段映射
        if (!empty($this->_map)) {
            foreach ($this->_map as $key => $val) {
                if (1 == $type) {
                    // 读取
                    if (isset($data[$val])) {
                        $data[$key] = $data[$val];
                        unset($data[$val]);
                    }
                } else {
                    if (isset($data[$key])) {
                        $data[$val] = $data[$key];
                        unset($data[$key]);
                    }
                }
            }
        }
        return $data;
    }

    /**
     * 创建数据对象 但不保存到数据库
     *
     * @param mixed $data 创建数据
     * @param string $type 状态
     * @param array $fields 需校验的字段 供DbModel使用,一般不用传
     * @return mixed
     */
    public function create($data, $type = '', array $fields = [])
    {
        if (is_object($data)) {
            $data = get_object_vars($data);
        }
        // 验证数据
        if (empty($data) || !is_array($data)) {
            $this->error = L('_DATA_TYPE_INVALID_');
            return false;
        }
        
        // 检测提交字段的合法性
        // 判断令牌验证字段
        if (C('TOKEN_ON')) {
            $fields[] = C('TOKEN_NAME', null, '__hash__');
        }

        if (!empty($fields)) {
            foreach ($data as $key => $val) {
                if (!in_array($key, $fields)) {
                    unset($data[$key]);
                }
            }
        }

        // 检查字段映射
        $data = $this->parseFieldsMap($data, 0);

        // 数据自动验证
        if (!$this->autoValidate($data, $type)) {
            return false;
        }

        // 表单令牌验证
        if (!$this->autoCheckToken($data)) {
            $this->error = L('_TOKEN_ERROR_');
            return false;
        }

        // 验证完成生成数据对象
        if ($this->autoCheckFields) {
            // 开启字段检测 则过滤非法字段数据
            $fields = $this->getFields();
            foreach ($data as $key => $val) {
                if (!in_array($key, $fields)) {
                    unset($data[$key]);
                } elseif (MAGIC_QUOTES_GPC && is_string($val)) {
                    $data[$key] = stripslashes($val);
                }
            }
        }

        // 创建完成对数据进行自动处理
        $this->autoOperation($data, $type);
        // 赋值当前数据对象
        $this->data = $data;
        // 返回创建的数据以供其他调用
        return $data;
    }
    
    /**
     * 构造auto条件
     * 
     * @param boolean|array $value array 自动填充规则,false代表不填充 
     * @return $this
     */
    public function auto($value)
    {
        $this->options['auto'] = $value;
        return $this;
    }
    
    /**
     * 构造validate条件
     * 
     * @param array $rules
     * @return $this
     */
    public function validate($rules)
    {
        $this->options['validate'] = $rules;
        return $this;
    }
    
    /**
     * 构造token条件 支持使用token(false) 关闭令牌验证
     * 
     * @param boolean $value
     * @return $this
     */
    public function token($value)
    {
        $this->options['token'] = $value;
        return $this;
    }

    /**
     * 获取字段信息
     * @return array
     */
    public function getFields()
    {
        return $this->fields;
    }

    /**
     * 自动表单令牌验证
     * TODO  ajax无刷新多次提交暂不能满足
     * @param array $data 表单数据
     * @return boolean
     */
    public function autoCheckToken($data)
    {
        if (isset($this->options['token']) && !$this->options['token']) {
            return true;
        }

        if (C('TOKEN_ON')) {
            $name = C('TOKEN_NAME', null, '__hash__');
            if (!isset($data[$name]) || !isset($_SESSION[$name])) {
                // 令牌数据无效
                return false;
            }

            // 令牌验证
            list($key, $value) = explode('_', $data[$name]);
            if (isset($_SESSION[$name][$key]) && $value && $_SESSION[$name][$key] === $value) {
                // 防止重复提交
                unset($_SESSION[$name][$key]); // 验证完成销毁session
                return true;
            }
            // 开启TOKEN重置
            if (C('TOKEN_RESET')) {
                unset($_SESSION[$name][$key]);
            }

            return false;
        }
        return true;
    }

    /**
     * 自动表单处理
     *
     * @param array $data 创建数据
     * @param string $type 创建类型
     * @return mixed
     */
    private function autoOperation(&$data, $type)
    {
        if (isset($this->options['auto']) && false === $this->options['auto']) {
            // 关闭自动完成
            return $data;
        }
        if (!empty($this->options['auto'])) {
            $_auto = $this->options['auto'];
            unset($this->options['auto']);
        } elseif (!empty($this->_auto)) {
            $_auto = $this->_auto;
        }
        // 自动填充
        if (isset($_auto)) {
            foreach ($_auto as $auto) {
                // 填充因子定义格式
                // array('field','填充内容','填充条件','附加规则',[额外参数])
                if (empty($auto[2])) {
                    $auto[2] = self::MODEL_INSERT;
                }
                // 默认为新增的时候自动填充
                if ($type == $auto[2] || self::MODEL_BOTH == $auto[2]) {
                    if (empty($auto[3])) {
                        $auto[3] = 'string';
                    }

                    switch (trim($auto[3])) {
                        case 'function'://  使用函数进行填充 字段的值作为参数
                        case 'callback':    // 使用回调方法
                            $args = isset($auto[4]) ? (array) $auto[4] : array();
                            if (isset($data[$auto[0]])) {
                                array_unshift($args, $data[$auto[0]]);
                            }
                            if ('function' == $auto[3]) {
                                $data[$auto[0]] = call_user_func_array($auto[1], $args);
                            } else {
                                $data[$auto[0]] = call_user_func_array(array(&$this, $auto[1]), $args);
                            }
                            break;
                        case 'field':    // 用其它字段的值进行填充
                            $data[$auto[0]] = $data[$auto[1]];
                            break;
                        case 'ignore':    // 为空忽略
                            if ($auto[1] === $data[$auto[0]]) {
                                unset($data[$auto[0]]);
                            }

                            break;
                        case 'string':
                        default:    // 默认作为字符串填充
                            $data[$auto[0]] = $auto[1];
                    }
                    if (isset($data[$auto[0]]) && false === $data[$auto[0]]) {
                        unset($data[$auto[0]]);
                    }
                }
            }
        }
        return $data;
    }

    /**
     * 自动表单验证
     * 
     * @param array $data 创建数据
     * @param string $type 创建类型
     * @return boolean
     */
    protected function autoValidate($data, $type)
    {
        if (isset($this->options['validate']) && false === $this->options['validate']) {
            // 关闭自动验证
            return true;
        }
        if (!empty($this->options['validate'])) {
            $_validate = $this->options['validate'];
            unset($this->options['validate']);
        } elseif (!empty($this->_validate)) {
            $_validate = $this->_validate;
        }
        // 属性验证
        if (isset($_validate)) {
            // 如果设置了数据自动验证则进行数据验证
            if ($this->patchValidate) {
                // 重置验证错误信息
                $this->error = array();
            }
            foreach ($_validate as $key => $val) {
                // 验证因子定义格式
                // array(field,rule,message,condition,type,when,params)
                // 判断是否需要执行验证
                if (empty($val[5]) || (self::MODEL_BOTH == $val[5] && $type < 3) || $val[5] == $type) {
                    if (0 == strpos($val[2], '{%') && strpos($val[2], '}')) {
                    // 支持提示信息的多语言 使用 {%语言定义} 方式
                        $val[2] = L(substr($val[2], 2, -1));
                    }

                    $val[3] = isset($val[3]) ? $val[3] : self::EXISTS_VALIDATE;
                    $val[4] = isset($val[4]) ? $val[4] : 'regex';
                    // 判断验证条件
                    switch ($val[3]) {
                        case self::MUST_VALIDATE:    // 必须验证 不管表单是否有设置该字段
                            if (false === $this->validateField($data, $val)) {
                                return false;
                            }

                            break;
                        case self::VALUE_VALIDATE:    // 值不为空的时候才验证
                            if ('' != trim($data[$val[0]])) {
                                if (false === $this->validateField($data, $val)) {
                                    return false;
                                }
                            }

                            break;
                        default:    // 默认表单存在该字段就验证
                            if (isset($data[$val[0]])) {
                                if (false === $this->validateField($data, $val)) {
                                    return false;
                                }
                            }
                    }
                }
            }
            // 批量验证的时候最后返回错误
            if (!empty($this->error)) {
                return false;
            }
        }
        return true;
    }

    /**
     * 验证表单字段 支持批量验证
     * 如果批量验证返回错误的数组信息
     *
     * @param array $data 创建数据
     * @param array $val 验证因子
     * @return boolean
     */
    protected function validateField($data, $val)
    {
        if ($this->patchValidate && isset($this->error[$val[0]])) {
            //当前字段已经有规则验证没有通过
            return;
        }

        if (false === $this->validateFieldItem($data, $val)) {
            if ($this->patchValidate) {
                $this->error[$val[0]] = $val[2];
            } else {
                $this->error = $val[2];
                return false;
            }
        }
        return;
    }

    /**
     * 根据验证因子验证字段`
     *
     * @param array $data 创建数据
     * @param array $val 验证因子
     * @return boolean
     */
    protected function validateFieldItem($data, $val)
    {
        switch (strtolower(trim($val[4]))) {
            case 'function':// 使用函数进行验证
            case 'callback':    // 调用方法进行验证
                $args = isset($val[6]) ? (array) $val[6] : array();
                if (is_string($val[0]) && strpos($val[0], ',')) {
                    $val[0] = explode(',', $val[0]);
                }

                if (is_array($val[0])) {
                    // 支持多个字段验证
                    foreach ($val[0] as $field) {
                        $_data[$field] = $data[$field];
                    }

                    array_unshift($args, $_data);
                } else {
                    array_unshift($args, $data[$val[0]]);
                }
                if ('function' == $val[4]) {
                    return call_user_func_array($val[1], $args);
                } else {
                    return call_user_func_array(array(&$this, $val[1]), $args);
                }
            case 'confirm':    // 验证两个字段是否相同
                return $data[$val[0]] == $data[$val[1]];
            default:    // 检查附加规则
                return (new Validator)->check($data[$val[0]], $val[1], $val[4]);
        }
    }

    /**
     * 得到当前的数据对象名称
     *
     * @return string
     */
    public function getModelName()
    {
        if (empty($this->name)) {
            $name = substr(get_class($this), 0, -strlen(C('DEFAULT_M_LAYER')));
            if ($pos = strrpos($name, '\\')) {
                //有命名空间
                $this->name = substr($name, $pos + 1);
            } else {
                $this->name = $name;
            }
        }
        return $this->name;
    }

    /**
     * 返回模型的错误信息
     *
     * @return string|array
     */
    public function getError()
    {
        return $this->error;
    }

    /**
     * 设置数据对象值
     *
     * @param mixed $data 数据
     * @return $this
     */
    public function data($data = '')
    {
        if ('' === $data && !empty($this->data)) {
            return $this->data;
        }
        if (is_object($data)) {
            $data = get_object_vars($data);
        } elseif (is_string($data)) {
            parse_str($data, $data);
        } elseif (!is_array($data)) {
            E(L('_DATA_TYPE_INVALID_'));
        }
        $this->data = $data;
        return $this;
    }

    /**
     * 设置模型的属性值
     * @param string $name 名称
     * @param mixed $value 值
     * @return $this
     */
    public function setProperty($name, $value)
    {
        if (property_exists($this, $name)) {
            $this->$name = $value;
        }

        return $this;
    }

}
